﻿using System;

using Storm.TextEditor.Editor.Text;
using Storm.TextEditor.Editor.Undo;
using Storm.TextEditor.Parser.Objects;

namespace Storm.TextEditor.Editor.Interacting
{
	/// <summary>
	/// Represents the manager of user selection in a TextEditor.
	/// </summary>
	public class Selection
	{
		#region Fields

		private TextRange      bounds = null;
		private TextEditorBase control = null;

		#region Events

		/// <summary>
		/// Occurs when the selection has changed.
		/// </summary>
		public event EventHandler Change = null;

		#endregion

		#endregion

		#region Properties

		/// <summary>
		/// Gets or sets the currently selected text. If some text is already selected, overwrites that text.
		/// </summary>
		public String Text
		{
			get
			{
				if (this.IsValid == false)
					return "";
				else
					return control.Document.GetRange(this.LogicalBounds);
			}
			set
			{
				if (this.Text == value) return;

				// Bug fix.
				string tmp = value.Replace(Environment.NewLine, "\n");
				tmp = tmp.Replace("\n", Environment.NewLine);
				value = tmp;

				TextPoint caretPos = control.Caret.Position;
				int caretX = caretPos.X;
				int caretY = caretPos.Y;

				control.Document.StartUndoCapture();
				this.DeleteSelection();

				control.Document.InsertText(value, caretPos.X, caretPos.Y);
				this.SelLength = value.Length;

				if (caretX != caretPos.X || caretY != caretPos.Y)
					control.Caret.Position = new TextPoint(this.Bounds.LastColumn, this.Bounds.LastRow);

				control.Document.EndUndoCapture();
				control.Document.InvokeChange();
			}
		}

		/// <summary>
		/// Gets the normalized positions of the selection.
		/// Swapping start and end values if the selection is reversed.
		/// </summary>
		public TextRange LogicalBounds
		{
			get
			{
				TextRange r = new TextRange();
				if (this.Bounds.FirstRow < this.Bounds.LastRow)
					return Bounds;
				else if (this.Bounds.FirstRow == this.Bounds.LastRow &&
					this.Bounds.FirstColumn < this.Bounds.LastColumn)
				{
					return Bounds;
				}
				else
				{
					r.FirstColumn = this.Bounds.LastColumn;
					r.FirstRow = this.Bounds.LastRow;

					r.LastColumn = this.Bounds.FirstColumn;
					r.LastRow = this.Bounds.FirstRow;

					return r;
				}
			}
		}

		/// <summary>
		/// Gets whether the current selection contains one or more characters.
		/// </summary>
		public bool IsValid
		{
			get
			{
				return (this.LogicalBounds.FirstColumn != this.LogicalBounds.LastColumn ||
					this.LogicalBounds.FirstRow != this.LogicalBounds.LastRow);
			}
		}

		/// <summary>
		/// Gets or sets the length of the selection.
		/// </summary>
		public int SelLength
		{
			get
			{
				TextPoint p1 = new TextPoint(this.Bounds.FirstColumn, this.Bounds.FirstRow);
				TextPoint p2 = new TextPoint(this.Bounds.LastColumn, this.Bounds.LastRow);

				int i1 = control.Document.PointToIntPos(p1);
				int i2 = control.Document.PointToIntPos(p2);

				return i2 - i1;
			}
			set { this.SelEnd = this.SelStart + value; }
		}

		/// <summary>
		/// Gets or sets the current selection end index. (SelStart + SelLength basically)
		/// </summary>
		public int SelEnd
		{
			get
			{
				TextPoint p = new TextPoint(this.Bounds.LastColumn, this.Bounds.LastRow);
				return control.Document.PointToIntPos(p);
			}
			set
			{
				TextPoint p = control.Document.IntPosToPoint(value);

				this.Bounds.LastColumn = p.X;
				this.Bounds.LastRow = p.Y;
			}
		}


		/// <summary>
		/// Gets or sets the current selection start index.
		/// </summary>
		public int SelStart
		{
			get
			{
				TextPoint p = new TextPoint(this.Bounds.FirstColumn, this.Bounds.FirstRow);
				return control.Document.PointToIntPos(p);
			}
			set
			{
				TextPoint p = control.Document.IntPosToPoint(value);

				this.Bounds.FirstColumn = p.X;
				this.Bounds.FirstRow = p.Y;
			}
		}

		/// <summary>
		/// Gets or sets the logical selection start index.
		/// </summary>
		public int LogicalSelStart
		{
			get
			{
				TextPoint p = new TextPoint(this.LogicalBounds.FirstColumn, this.LogicalBounds.FirstRow);
				return control.Document.PointToIntPos(p);
			}
			set
			{
				TextPoint p = control.Document.IntPosToPoint(value);

				this.Bounds.FirstColumn = p.X;
				this.Bounds.FirstRow = p.Y;
			}
		}

		/// <summary>
		/// Gets or sets the selection bounds as a TextRange. 
		/// </summary>
		public TextRange Bounds
		{
			get { return bounds; }
			set
			{
				if (bounds != null)
					bounds.Change -= new EventHandler(OnBoundsChange);

				bounds = value;
				bounds.Change += new EventHandler(OnBoundsChange);
				this.OnChange();
			}
		}

		#endregion

		#region Methods

		#region Public

		/// <summary>
		/// Increases the level of indentation at the current selection by one.
		/// </summary>
		public void Indent()
		{
			if (this.IsValid == false)
				return;

			Row row = null;
			UndoBlockCollection actionGroup = new UndoBlockCollection();
			for (int i = this.LogicalBounds.FirstRow; i <= this.LogicalBounds.LastRow; i++)
			{
				row = control.Document[i];

				UndoBlock undoBlock = new UndoBlock();
				undoBlock.Action = UndoAction.InsertRange;

				if (control.KeepTabs == true)
				{
					row.Text = "\t" + row.Text;
					undoBlock.Text = "\t";
				}
				else
				{
					string indent = new string(' ', control.TabSpaces);
					row.Text = indent + row.Text;
					undoBlock.Text = indent;
				}

				undoBlock.Position.X = 0;
				undoBlock.Position.Y = i;

				actionGroup.Add(undoBlock);
			}

			if (actionGroup.Count > 0)
				control.Document.AddToUndoList(actionGroup);

			this.Bounds = this.LogicalBounds;
			this.Bounds.FirstColumn = 0;
			this.Bounds.LastColumn = row.Text.Length;

			control.Caret.Position.X = this.LogicalBounds.LastColumn;
			control.Caret.Position.Y = this.LogicalBounds.LastRow;
		}

		/// <summary>
		/// Decreases the level of indentation at the current selection by one.
		/// </summary>
		public void Outdent()
		{
			if (this.IsValid == false)
				return;

			Row row = null;
			UndoBlockCollection actionGroup = new UndoBlockCollection();
			for (int i = this.LogicalBounds.FirstRow; i <= this.LogicalBounds.LastRow; i++)
			{
				row = control.Document[i];

				UndoBlock undoBlock = new UndoBlock();
				undoBlock.Action = UndoAction.DeleteRange;

				undoBlock.Position.X = 0;
				undoBlock.Position.Y = i;

				actionGroup.Add(undoBlock);

				string s = row.Text;
				if (s.StartsWith("\t") == true)
				{
					undoBlock.Text = s.Substring(0, 1);
					s = s.Substring(1);
				}

				if (s.StartsWith(new string(' ', control.TabSpaces)) == true)
				{
					undoBlock.Text = s.Substring(0, control.TabSpaces);
					s = s.Substring(control.TabSpaces);
				}

				row.Text = s;
			}

			if (actionGroup.Count > 0)
				control.Document.AddToUndoList(actionGroup);

			this.Bounds = this.LogicalBounds;
			this.Bounds.FirstColumn = 0;
			this.Bounds.LastColumn = row.Text.Length;

			control.Caret.Position.X = this.LogicalBounds.LastColumn;
			control.Caret.Position.Y = this.LogicalBounds.LastRow;
		}

		/// <summary>
		/// Adds a string to the current selection.
		/// </summary>
		/// <param name="pattern">The string that will be added to the selection.</param>
		public void Indent(string pattern)
		{
			if (this.IsValid == false)
				return;

			Row row = null;
			UndoBlockCollection actionGroup = new UndoBlockCollection();
			for (int i = this.LogicalBounds.FirstRow; i <= this.LogicalBounds.LastRow; i++)
			{
				row = control.Document[i];
				row.Text = pattern + row.Text;

				UndoBlock undoBlock = new UndoBlock();
				undoBlock.Action = UndoAction.InsertRange;
				undoBlock.Text = pattern;

				undoBlock.Position.X = 0;
				undoBlock.Position.Y = i;

				actionGroup.Add(undoBlock);
			}

			if (actionGroup.Count > 0)
				control.Document.AddToUndoList(actionGroup);

			this.Bounds = this.LogicalBounds;
			this.Bounds.FirstColumn = 0;
			this.Bounds.LastColumn = row.Text.Length;

			control.Caret.Position.X = this.LogicalBounds.LastColumn;
			control.Caret.Position.Y = this.LogicalBounds.LastRow;
		}

		/// <summary>
		/// Adds a string to the current selection.
		/// </summary>
		/// <param name="pattern">The string that will be added to the selection.</param>
		public void Outdent(string pattern)
		{
			if (this.IsValid == false)
				return;

			Row row = null;
			UndoBlockCollection actionGroup = new UndoBlockCollection();
			for (int i = this.LogicalBounds.FirstRow; i <= this.LogicalBounds.LastRow; i++)
			{
				row = control.Document[i];

				UndoBlock undoBlock = new UndoBlock();
				undoBlock.Action = UndoAction.DeleteRange;
				undoBlock.Position.X = 0;
				undoBlock.Position.Y = i;

				actionGroup.Add(undoBlock);

				string s = row.Text;
				if (s.StartsWith(pattern) == true)
				{
					undoBlock.Text = s.Substring(0, pattern.Length);
					s = s.Substring(pattern.Length);
				}

				row.Text = s;
			}

			if (actionGroup.Count > 0)
				control.Document.AddToUndoList(actionGroup);

			this.Bounds = this.LogicalBounds;
			this.Bounds.FirstColumn = 0;
			this.Bounds.LastColumn = row.Text.Length;

			control.Caret.Position.X = this.LogicalBounds.LastColumn;
			control.Caret.Position.Y = this.LogicalBounds.LastRow;
		}

		/// <summary>
		/// Delete the active selection.
		/// <seealso cref="ClearSelection"/>
		/// </summary>
		public void DeleteSelection()
		{
			TextRange r = this.LogicalBounds;

			int x = r.FirstColumn;
			int y = r.FirstRow;

			control.Document.DeleteRange(r);
			control.Caret.Position.X = x;
			control.Caret.Position.Y = y;

			this.ClearSelection();
			control.ScrollIntoView();
		}

		/// <summary>
		/// Clear the active selection.
		/// <seealso cref="DeleteSelection"/>
		/// </summary>
		public void ClearSelection()
		{
			if (control != null)
			{
				this.Bounds.FirstColumn = control.Caret.Position.X;
				this.Bounds.FirstRow = control.Caret.Position.Y;

				this.Bounds.LastColumn = control.Caret.Position.X;
				this.Bounds.LastRow = control.Caret.Position.Y;
			}
		}

		/// <summary>
		/// Sets the bounds of the selection to the Caret start and end positions.
		/// </summary>
		public void MakeSelection()
		{
			this.Bounds.LastColumn = control.Caret.Position.X;
			this.Bounds.LastRow = control.Caret.Position.Y;
		}

		/// <summary>
		/// Selects all text in the TextEditor.
		/// </summary>
		public void SelectAll()
		{
			this.Bounds.FirstColumn = 0;
			this.Bounds.FirstRow = 0;

			this.Bounds.LastColumn = control.Document[control.Document.Count - 1].Text.Length;
			this.Bounds.LastRow = control.Document.Count - 1;

			control.Caret.Position.X = this.Bounds.LastColumn;
			control.Caret.Position.Y = this.Bounds.LastRow;
			control.ScrollIntoView();
		}

		#endregion

		#region EventHandlers

		private void OnBoundsChange(object s, EventArgs e)
		{
			this.OnChange();
		}

		private void OnPositionChange(object s, EventArgs e)
		{
			this.OnChange();
		}

		/// <summary>
		/// Raises the Change event.
		/// </summary>
		private void OnChange()
		{
			if (this.Change != null)
				this.Change(this, null);
		}

		#endregion

		#endregion

		/// <summary>
		/// Initializes a new instance of Selection.
		/// </summary>
		public Selection(TextEditorBase control)
		{
			this.control = control;
			this.Bounds = new TextRange();
		}
	}
}
